// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Xml;

using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Server;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Proxy;


namespace Microsoft.TeamFoundation.Migration.Toolkit.Wit
{
    /// <summary>
    /// TFS migration store.
    /// </summary>
    class TfsMigrationWorkItemStore : IMigrationWorkItemStore, IWorkItemUpdatePackageFactory, IComparer<Identity>
    {
        private TfsCore m_core;                             // Shared TFS core
        private StringComparer m_stringValueComparer;       // Comparer for fields' values of string type
        private StringComparer m_fieldNameComparer;         // Comparer for field names
        private XmlDocument m_doc;                          // XML document for generating update packages

        private WorkItemStore m_store;                      // Work item store
        private ClientService m_clientService;              // WIT service


        /// <summary>
        /// Obtains collection of items scheduled for synchronization.
        /// </summary>
        /// <param name="highWatermark">High watermark object</param>
        /// <returns>Collection of items</returns>
        public IEnumerable<IMigrationWorkItem> GetItems(
            ref string highWatermark)
        {
            // Create condition
            StringBuilder c = new StringBuilder("[System.TeamProject]=@Project");
            if (!string.IsNullOrEmpty(m_core.Config.Filter))
            {
                c.AppendFormat(CultureInfo.InvariantCulture, " AND ({0})", m_core.Config.Filter);
            }

            if (!string.IsNullOrEmpty(highWatermark))
            {
                DateTime dt = Convert.ToDateTime(highWatermark, CultureInfo.InvariantCulture);
                dt = dt.AddSeconds(-15);
                c.AppendFormat(CultureInfo.InvariantCulture, " AND [System.ChangedDate] >= '{0:u}'", dt);
            }

            TfsMigrationWorkItems items = new TfsMigrationWorkItems(m_core, WorkItemStore, c.ToString());
            highWatermark = items.AsOf.ToString(CultureInfo.InvariantCulture);
            return items;
        }

        /// <summary>
        /// Obtains collection of items scheduled for synchronization.
        /// </summary>
        /// <param name="ids">Id of items</param>
        public GetWorkItemResult[] GetItems(
            string[] ids)
        {
            if (ids == null)
            {
                throw new ArgumentNullException("ids");
            }
            GetWorkItemResult[] results = new GetWorkItemResult[ids.Length];

            if (ids.Length == 0)
            {
                return results;
            }

            // Do a query
            BatchReadParameterCollection batchParams = new BatchReadParameterCollection();
            for (int i = 0; i < ids.Length; i++)
            {
                try
                {
                    if (string.IsNullOrEmpty(ids[i]))
                    {
                        throw new ToolkitArgException("ids");
                    }
                    int intId = Convert.ToInt32(ids[i], CultureInfo.InvariantCulture);
                    batchParams.Add(new BatchReadParameter(intId));
                }
                catch (Exception e)
                {
                    results[i] = new GetWorkItemResult(ids[i], e);
                }
            }

            if (batchParams.Count > 0)
            {
                WorkItemCollection tfsItems = WorkItemStore.Query(batchParams, "SELECT [System.Id], [System.Rev] FROM WorkItems");
                Debug.Assert(tfsItems.Count == ids.Length, "Wrong number of items returned by the query");

                for (int i = 0; i < ids.Length; i++)
                {
                    try
                    {
                        WorkItem tfsItem = tfsItems[i];
                        results[i] = new GetWorkItemResult(
                            Convert.ToString(tfsItem.Id, CultureInfo.InvariantCulture),
                            new TfsMigrationWorkItem(m_core, tfsItem));
                    }
                    catch (Exception e)
                    {
                        results[i] = new GetWorkItemResult(ids[i], e);
                    }
                }
            }

            return results;
        }

        /// <summary>
        /// Submits updates.
        /// </summary>
        /// <param name="updates">Updates to submit</param>
        /// <returns>Update results (one for each update)</returns>
        public UpdateResult[] Update(
            IWorkItemUpdate[] updates)
        {
            Debug.Assert(updates.Length != 0, "Empty batch!");
            return TfsBatchUpdateHelper.Submit(m_core, ClientService, updates);
        }

        /// <summary>
        /// Verifies settings.
        /// </summary>
        /// <param name="session">Target session</param>
        /// <param name="tfsStore">TFS store on the opposite side</param>
        public void VerifySettings(WorkItemTrackingSession session, WorkItemStore tfsStore)
        {
            if (session.TfsSettingsVerifier != null)
            {
                session.TfsSettingsVerifier(session, tfsStore);
            }
        }

        /// <summary>
        /// Reopens store from a different thread.
        /// </summary>
        /// <returns>Store safe to work with from the calling thread</returns>
        public IMigrationWorkItemStore Reopen()
        {
            return new TfsMigrationWorkItemStore(m_core);
        }

        /// <summary>
        /// Synchronizes own metadata with the given project.
        /// </summary>
        /// <param name="project">Project</param>
        public void SynchronizeMetadata(
            Project project)
        {
            MetadataTypes types = m_core.Config.MetadataSynchronizationPolicy.Types;
            if ((types & MetadataTypes.Lists) != 0)
            {
                SyncGlobalLists(project);
            }

            if ((types & MetadataTypes.Accounts) != 0)
            {
                SyncAccounts(project);
            }

            if ((types & MetadataTypes.Types) != 0)
            {
                SyncWorkItemTypes(project);
            }
        }

        /// <summary>
        /// Gets the list of flat fields for the given work item type.
        /// </summary>
        /// <param name="workItemType">Work item type</param>
        /// <returns>All flat fields</returns>
        public IEnumerable<string> GetFlatFields(
            string workItemType)
        {
            foreach (string name in m_core.Config.FlatFields)
            {
                if (WorkItemStore.FieldDefinitions.Contains(name))
                {
                    var def = m_store.FieldDefinitions[name];
                    yield return m_core.Config.FieldForm == TfsFieldForm.Friendly ? def.Name : def.ReferenceName;
                }
            }
        }

        /// <summary>
        /// Checks whether specified user exists in the store.
        /// </summary>
        /// <param name="name">User name</param>
        /// <returns>True if the user exists</returns>
        public bool IsValidUser(
            string name)
        {
            return TfsCore.IsValidUser(name);
        }

        /// <summary>
        /// Checks whether a field is valid for a work item type.
        /// </summary>
        /// <param name="workItemType">Work item type</param>
        /// <param name="fieldName">Field name</param>
        /// <returns>True if the field is valid for the work item type</returns>
        public bool IsValidField(
            string workItemType,
            string fieldName)
        {
            return m_core.IsValidField(workItemType, fieldName);
        }

        /// <summary>
        /// Synchronizes work item types.
        /// </summary>
        /// <param name="typeName">Type name in this system</param>
        /// <param name="tfsTypeName">Type name in the system on the opposite side</param>
        /// <param name="tfsTypes">Collection of types from the opposite side</param>
        public void SynchronizeWorkItemType(
            string typeName,
            string tfsTypeName,
            WorkItemTypeCollection tfsTypes)
        {
            // This method does nothing. Type synchronization in TFS 2 TFS scenario occurs in 
            // store's SyncMetadata method.
        }

        /// <summary>
        /// Refreshes store's data.
        /// </summary>
        public void Refresh()
        {
            if (m_store != null)
            {
                m_store.SyncToCache();
            }
        }

        /// <summary>
        /// Closes the work item store.
        /// </summary>
        public void Close()
        {
            m_store = null;

            if (m_clientService != null)
            {
                m_clientService.Dispose();
                m_clientService = null;
            }
        }

        /// <summary>
        /// Returns flags describing the work item store.
        /// </summary>
        public MigrationWorkItemData Flags { get { return MigrationWorkItemData.All; } }

        /// <summary>
        /// Gets comparer used with string values.
        /// </summary>
        public StringComparer StringValueComparer { get { return m_stringValueComparer; } }

        /// <summary>
        /// Gets comparer used with field names.
        /// </summary>
        public StringComparer FieldNameComparer { get { return m_fieldNameComparer;} }

        /// <summary>
        /// Gets comparer for work items' ids.
        /// </summary>
        public StringComparer IdComparer { get { return StringComparer.InvariantCultureIgnoreCase; } }

        /// <summary>
        /// Gets the change list factory.
        /// </summary>
        public IWorkItemUpdatePackageFactory ChangeListFactory { get { return (IWorkItemUpdatePackageFactory)this; } }

        /// <summary>
        /// Creates a change list for the given work item.
        /// </summary>
        /// <param name="source">Source work item</param>
        /// <param name="item">Work item</param>
        /// <returns>Change list object</returns>
        public IWorkItemUpdatePackage CreateUpdatePackage(
            IMigrationWorkItem source,
            IMigrationWorkItem item)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }
            return new TfsWorkItemUpdatePackage(m_core, m_doc, (TfsMigrationWorkItem)item, source.Watermark.Id, ClientService);
        }

        /// <summary>
        /// Creates a change list for a new work item of the given type.
        /// </summary>
        /// <param name="source">Source work item</param>
        /// <param name="workItemType">Work item type</param>
        /// <returns>Change list object</returns>
        public IWorkItemUpdatePackage CreateUpdatePackage(
            IMigrationWorkItem source,
            string workItemType)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }
            if (string.IsNullOrEmpty(workItemType))
            {
                throw new ArgumentNullException("workItemType");
            }
            return new TfsWorkItemUpdatePackage(m_core, m_doc, workItemType, source.Watermark.Id, ClientService);
        }

        /// <summary>
        /// Gets name of the system.
        /// </summary>
        public string SystemName { get { return "TFS"; } }

        /// <summary>
        /// Gets datastore name.
        /// </summary>
        public string StoreName { get { return m_core.ServerName; } }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="core">Shared core</param>
        internal TfsMigrationWorkItemStore(
            TfsCore core)
        {
            m_core = core;
            m_doc = new XmlDocument();

            //$TODO_VNEXT: it would be nice to compare string values in server locale. However,
            // that property is unavailable in the current version.
            m_stringValueComparer = StringComparer.InvariantCultureIgnoreCase;
            m_fieldNameComparer = StringComparer.InvariantCultureIgnoreCase;
        }

        /// <summary>
        /// Returns shared TFS core object.
        /// </summary>
        internal TfsCore Core { get { return m_core; } }

        /// <summary>
        /// Gets the underlying work item store object.
        /// </summary>
        internal WorkItemStore WorkItemStore
        {
            get
            {
                if (m_store == null)
                {
                    m_store = m_core.CreateWorkItemStore();
                }
                return m_store;
            }
        }

        /// <summary>
        /// Returns work item tracking service.
        /// </summary>
        private ClientService ClientService
        {
            get
            {
                if (m_clientService == null)
                {
                    m_clientService = m_core.CreateClientService();
                }
                return m_clientService;
            }
        }

        /// <summary>
        /// Synchronizes own global lists with those from the project.
        /// </summary>
        /// <param name="p">Source project</param>
        private void SyncGlobalLists(
            Project p)
        {
            ReadOnlyCollection<string> ignoredLists = m_core.Config.MetadataSynchronizationPolicy.IgnoredLists;
            XmlDocument doc = p.Store.ExportGlobalLists();

            XmlNodeList lists = doc.DocumentElement.SelectNodes("GLOBALLIST");
            foreach (XmlElement list in lists)
            {
                string name = list.GetAttribute("name");
                int i = 0;

                for (i = 0; i < ignoredLists.Count; i++)
                {
                    if (StringValueComparer.Equals(name, ignoredLists[i]))
                    {
                        list.ParentNode.RemoveChild(list);
                        break;
                    }
                }
            }

            WorkItemStore.ImportGlobalLists(doc.DocumentElement);
        }

        /// <summary>
        /// Synchronizes own accounts with those from the project.
        /// </summary>
        /// <param name="p">Source project</param>
        private void SyncAccounts(
            Project p)
        {
            // Obtain global- and project-scoped URI's from the source project
            IGroupSecurityService srcGss = (IGroupSecurityService)p.Store.TeamFoundationServer.GetService(
                typeof(IGroupSecurityService));

            Identity[] srcGlobGroups = LoadGroups(srcGss, null);
            Identity[] srcProjGroups = LoadGroups(srcGss, p.Uri.ToString());

            IGroupSecurityService dstGss = (IGroupSecurityService)WorkItemStore.TeamFoundationServer.GetService(
                typeof(IGroupSecurityService));
            string dstProjUri = WorkItemStore.Projects[m_core.Config.Project].Uri.ToString();

            Identity[] dstGlobGroups = LoadGroups(dstGss, null);
            Identity[] dstProjGroups = LoadGroups(dstGss, dstProjUri);

            SymDiff<Identity> globs = new SymDiff<Identity>(srcGlobGroups, dstGlobGroups, this);
            SymDiff<Identity> projs = new SymDiff<Identity>(srcProjGroups, dstProjGroups, this);

            CreateGroups(dstGss, null, globs.LeftOnly);
            CreateGroups(dstGss, dstProjUri, projs.LeftOnly);

        }

        /// <summary>
        /// Synchronizes own work item types with those from the project.
        /// </summary>
        /// <param name="p">Source project</param>
        private void SyncWorkItemTypes(
            Project p)
        {
            ReadOnlyCollection<WorkItemTypeMapping> mappings = m_core.Config.Session.WorkItemTypeMappings;
            ReadOnlyCollection<string> ignoredTypes = m_core.Config.MetadataSynchronizationPolicy.IgnoredTypes;

            foreach (WorkItemType t in p.WorkItemTypes)
            {
                XmlDocument doc = t.Export(false);

                // Is this type mapped?
                XmlElement typeElement = (XmlElement)doc.DocumentElement.SelectSingleNode("WORKITEMTYPE");
                string name = typeElement.GetAttribute("name");
                int i;

                for (i = 0; i < mappings.Count; i++)
                {
                    WorkItemTypeMapping map = mappings[i];
                    if (StringValueComparer.Equals(map.TfsName, name))
                    {
                        name = map.OtherName;
                        typeElement.SetAttribute("name", name);
                        break;
                    }
                }

                // Is this type ignored?
                for (i = 0; i < ignoredTypes.Count; i++)
                {
                    if (StringValueComparer.Equals(ignoredTypes[i], name))
                    {
                        break;
                    }
                }

                if (i >= ignoredTypes.Count)
                {
                    WorkItemStore.Projects[m_core.Config.Project].WorkItemTypes.Import(doc.DocumentElement);
                }
            }
        }

        /// <summary>
        /// Loads all groups from the given project.
        /// </summary>
        /// <param name="gss">GSS service</param>
        /// <param name="uri">Project's URI. Passing null will return array of global groups</param>
        /// <returns>Array of groups</returns>
        private Identity[] LoadGroups(
            IGroupSecurityService gss,
            string uri)
        {
            Identity[] groups = gss.ListApplicationGroups(uri);
            Array.Sort(groups, this);
            return groups;
        }
        
        /// <summary>
        /// Comparer for identity type.
        /// </summary>
        /// <param name="i1">Identity 1</param>
        /// <param name="i2">Identity 2</param>
        /// <returns>Results of comparison</returns>
        int IComparer<Identity>.Compare(
            Identity x,
            Identity y)
        {
            Debug.Assert(x != null && y != null, "Null identity!");

            if (x.Deleted)
            {
                if (!y.Deleted) return -1;
            }
            else if (y.Deleted)
            {
                return 1;
            }

            return TFStringComparer.UserName.Compare(x.DisplayName, y.DisplayName);
        }

        /// <summary>
        /// Creates given groups.
        /// </summary>
        /// <param name="gss">GSS service</param>
        /// <param name="projUri">Uri of the project under which groups must be created. Specifying NULL here will
        /// result in creation of global groups</param>
        /// <param name="groups">Groups that must be created</param>
        private void CreateGroups(
            IGroupSecurityService gss,
            string projUri,
            List<Identity> groups)
        {
            for (int i = 0; i < groups.Count; i++)
            {
                Identity g = groups[i];

                if (!g.Deleted)
                {
                    try
                    {
                        gss.CreateApplicationGroup(projUri, g.DisplayName, g.Description);
                    }
                    catch (Exception e)
                    {
                        string msg = string.Format(
                            Resources.Culture,
                            Resources.ErrorFailedGroupCreation,
                            g.DisplayName,
                            WorkItemStore.TeamFoundationServer.Name);
                        throw new WitMigrationException(msg, e);
                    }
                }
            }
        }
    }
}
